\subsection{hello world漫游}

本节希望通过显示“hello world!”的应用程序来感受操作系统的功能和特征。一般应用程序都希望能够完成接受输入、显示输出、保存结果、定时处理、程序执行切换等基本功能，而这些功能都是在操作系统的帮助下完成的。

\subsubsection{智人时代下的"hello　world!"}

智人时代的典型操作系统是Linux和Windows。Linux和Windows都非常强大，在这两个操作系统上编写和执行“显示“hello world!”的各种应用程序很容易，容易到你都感觉不到操作系统的存在。相对而言，Linux可以看到它的源代码，用到的各种工具和操作相对也原始一些，可以让读者与操作系统的距离感更近一些，对于理解操作系统的实现会有很大帮助，所以我们以Linux作为我们的实验环境。
接下来的五个小实验，分别基于不同的需求完成对“hello world!”的显示：
\begin{itemize}
	\item 在显示器上直接显示字符串；
	\item 通过定时器在显示器上定时显示字符串；
	\item 把字符串“显示”到磁盘上永久保存；
	\item 可根据键盘输入改变字符串的显示内容；
	\item 定时显示程序和交互显示程序一起执行。
\end{itemize}

通过这些实验，我们可以看到操作系统的一个重要功能是给应用程序提供了便捷的访问服务，应用程序只需发出要求，比如显示字符串、保存文件、定时唤醒、接收输入等，操作系统就可以帮助它完成；另外还有一些很重要的看不见的管理服务，比如给应用程序提供执行环境，切换应用程序，回收应用程序占用的资源等，这些就不能直接地感受到了。而对于硬件，比如显示器、定时器、磁盘、键盘等各种外设，操作系统都把它们很好地管理起来，提供了外设管理服务，使得我们的应用程序很方便地通过一些简单的AP就可以访问这些外设，而不需要关注外设的复杂控制细节。


\begin{note} 
下面的实验环境是基于ubuntu-16.04 x86-64环境，虽然CPU是x86-64，当下面的实验完全没有涉及到x86-64的细节，所以在这里就直接忽视让操作系统和应用正常运行的这个不可或缺的一个或多个CPU和其他不相关的硬件细节。
\end{note}

\paragraph{显示"hello　world!"}
让我们通过智人时代的操作系统Linux来感受一下显示一个字符串的过程。假定读者建立了Linux实验环境(参见\ref{setuplinux})，对Ｃ语言有一定的了解，所以可以写出如下的代码 helloworld.c：
\begin{lstlisting}[language={C}]
void main(void)
{
  puts("hello world!");
}
\end{lstlisting}

并在Linux环境中的shell界面下，执行gcc编译命令，把helloworld.c转换成执行程序helloworld：

\begin{lstlisting}[language={bash}]
$ gcc -o helloworld helloworld.c
\end{lstlisting}

在shell界面下，在生成执行程序helloworld的目录下执行此程序，可以看到程序显示出了“hello world!”：
%\begin{lstlisting}[language={bash},numbers=none]
\begin{lstlisting}[language={bash}]
	  $ helloworld
	  hello world!
\end{lstlisting}


只要读者会基本编程，对于上面的输出结果，应该不会感到陌生。但读者对具体的执行过程了解吗？Linux操作系统和它用的x86计算机硬件太复杂，如果要详细分析和解释上面示例的两行显示背后的具体执行过程的细节，我们可以写出一本超过1000页的大部头。考虑到读者时间有限，下面我们将站在操作系统的角度来简单理解一下这个helloworld程序的执行过程。

%上面的操作过程，需要与人交互的有两个外设，一个是键盘，一个是显示器。首先，你看到的是\$符号，这是一个正在运行的程序shell的人机交互界面。在你没敲字符的时候，\textbf{shell处于睡觉状态}。当你通过键盘敲入“g”和后续的多个字符的时候，首先是操作系统收到键盘发出的字符，然后通知shell，有字符来了！shell本来在睡觉，被操作系统唤醒后，接收字符，并发出显示字符的请求给操作系统。操作系统收到shell的请求后，把字符显示到显示器上，然后通知shell完成显示字符任务了。当shell程序收到回车字符的时候，就开始把整个字符串看成是一个命令，解析完此命令后，并告知操作系统，继续请操作系统帮忙执行另外一个程序gcc来完成整个编译过程。操作系统为此需要创建一个让gcc可以正常工作的执行空间，并启动gcc程序，让它能够完成整个编译过程。gcc于是开始干活，首先请操作系统把helloworl操作系统d.c这个文件从磁盘上读到内存中，gcc对内存中的helloworld.c的内容进行编译，生成helloworld执行程序，但此时这个程序还在内存中。于是gcc继续请操作系统帮忙，把这个helloworld执行程序写到磁盘上。当你看到第二个\$符号出现的时候，表示gcc的工作完成了。

%上面的helloworld执行程序需要用到一个个外设：显示器。dang\$上继续敲如字符串"hello　world!"，并回车。类似上面的描述，这次shell程序会请求操作系统来执行helloworld这个程序。操作系统为此需要创建一个让helloworld程序可以正常工作的执行空间，并启动helloworld程序。helloworld的执行工作就是显示字符串“hello world!”。为此，它像shell一样，给操作系统发出显示字符串的请求。操作系统收到显示字符串的请求后，把字符串显示到屏幕上。至此，上面示例中三行显示的背后执行过程就简单描述完毕。

上面的helloworld执行程序需要用到一个外设：显示器。当你在shell的\$提示符后继续敲如字符串"hello　world!"并回车，shell解析此命令，分析出你是要执行helloworld这个程序，于是会请求操作系统来执行helloworld这个程序。操作系统为此需要创建一个让helloworld程序可以正常工作的执行环境(比如分配内存放代码和数据，提供CPU用于执行等)，并启动helloworld程序。此时shell程序要让位helloworld程序执行，为此操作系统还要完成一个执行上下文切换的过程，让helloworld能够占用CPU来执行。完成切换后，helloworld程序才真正开始执行。

helloworld的执行工作就是显示字符串“hello world!”。为此，它像shell一样，给操作系统发出显示字符串的请求。操作系统收到显示字符串的请求后，直接控制显卡这个设备，通过显卡把字符显示到显示器上，然后通知shell完成显示字符任务了。helloworld程序执行完毕后，操作系统还要把之前分配给helloworld的执行环境给收回，用于其他程序的执行。至此，上面示例中两行行显示的背后执行过程就简单描述完毕。

\begin{note} 
操作系统其实也没有直接控制显示器，而是通过控制显卡，让显卡访问显示器上显示信息的。
\end{note}

\paragraph{每秒定时显示"hello　world!"}
如果要每秒定时显示字符串，很显然需要时钟外设来帮助实现定时。添加一点代码形成timing-helloworld.c，就可以实现定时显示helloworld了。
\begin{lstlisting}[language={C}]
void main(void)
{
　　while(1) {
　　　　sleep(1);
　　　　puts("hello world!");
　　}
}
\end{lstlisting}

在Linux环境中，执行gcc编译命令，把timing-helloworld.c转换成执行程序timing-helloworld，并执行生成的执行程序timing-helloworld：
\begin{lstlisting}[language={bash}]
	$ gcc -o timing-helloworld timing-helloworld.c
	$ timing-helloworld
	hello world!
	hello world!
	......
\end{lstlisting}

可以看到，当执行timing-helloworld程序时，屏幕上会每隔一秒重复显示“helloworld”。新增加的sleep函数完成了等待一秒并恢复执行的功能。其实这个功能也是靠藏在后面的操作系统帮忙完成的。当timing-hellworld执行sleep(1)函数时，它向操作系统发出了一个请求，要求操作系统先让它睡觉，且让操作系统帮它设个1秒到期的闹钟(更正式的说法是定时器)。于是操作系统先把timing-helloworld设置为睡眠状态，且对时钟外设做好配置，让它1秒中后产生一个中断，通知操作系统到点了。操作系统做完这两件事后，就忙自己的其他事情并安排调度其他程序运行。过了1秒后，时钟外设产生了一个中断，通知操作系统到点了，操作系统响应这个中断，并记得timing-hellworld需要被唤醒并继续运行，于是就把timing-hellworld唤醒，并让它继续运行。这样，timing-hellworld就开始每隔1秒显示字符串了。

\paragraph{把"hello world!"字符串存到磁盘上}

如果要把显示字符串长久保存下来，很显然需要磁盘外设来帮助实现长期存储的功能。添加一点代码形成file-helloworld.c，就可以实现把字符串保存到磁盘上了。

\begin{lstlisting}[language={C}]
#include <stdio.h>
void main(void){
  FILE *fp;
  fp = fopen("file-helloworld.txt", "w");
  fputs("hello world!",fp);
  fclose(fp);
}
\end{lstlisting}

在Linux环境中，执行gcc编译命令，把file-helloworld.c转换成执行程序file-helloworld，并执行生成的执行程序file-helloworld：
\begin{lstlisting}[language={bash}]
	$ gcc -o file-helloworld file-helloworld.c
	$ file-helloworld
	$ more file-helloworld.txt
	hello world!	
\end{lstlisting}

可以看到，当执行file-helloworld程序时，当前目录下多了一个文件file-helloworld.txt，通过more命令，可以看到file-helloworld.txt文件的内容就是我们需要保存的字符串"hello　world!"。这里我们可以看到通过操作系统，应用程序可用文件的形式方便地把字符串存储到磁盘上，而没有关注磁盘磁盘的细节。当执行程序file-helloworld的时候，操作系统做了啥呢？首先，当file-helloworld执行fopen函数时，会请求操作系统在当前目录下创建一个可写的文件file-helloworld.txt。于是操作系统会定位到当前目录在磁盘上的位置，并在此目录下添加一个文件，此时的文件内容为空。然后当file-helloworld执行fput函数时，会请求操作系统把"hello　world!"这个内容写到file-helloworld.txt文件中。于是操作系统定位到file-helloworld.txt文件在磁盘中的位置，给这个文件分配空闲磁盘扇区空间用于存放文件内容，再把位于内存中的字符串"hello　world!"以磁盘块为单位，写入到文件内容对应的磁盘扇区中。



\paragraph{交互式显示"hello world!"}

上面三个小实验缺少了一点与人的交互。比如，如果我敲了一个名字“human”，程序就能显示"human, hello world!"。让程序能接收输入，那还需要一个外设：键盘。通过键盘，程序就可以得到人的输入了。getchar-helloworld.c的代码如下：

\begin{lstlisting}[language={C}]
void main(void)
{
  char name[100];
  int i=0;
  while((name[i] = getchar())!='\n' && i<80)
        i++ ;
  name[i]=0;
  strcat(name,", hello world!");
  puts(name);
}
\end{lstlisting}

在Linux环境中，执行gcc编译命令，把getchar-helloworld.c转换成执行程序getchar-helloworld，并执行生成的执行程序getchar-helloworld：
\begin{lstlisting}[language={bash}]
	$ gcc -o getchar-helloworld getchar-helloworld.c
	$ getchar-helloworld
	human
	human, hello world!	
\end{lstlisting}

当执行getchar-helloworld程序时，如果你没敲字符，getchar-helloworld就会默默地等待你的输入\textbf{其实getchar-helloworld处于睡觉状态}。当你通过键盘敲入“h”和后续的多个字符的时候，首先是操作系统收到键盘发出的字符，然后唤醒并通知getchar-helloworld，有字符来了。getchar-helloworld本来在睡觉，被操作系统唤醒后，持续接收字符。在收到'\\n'回车符后，就把用户输入的字符与", hello world!"字符连接在一起，形成一个新字符串，并请求操作系统显示这个新字符串。操作系统接下来的过程与上面的分析解释是一样的，。

\paragraph{定时＋交互式显示"hello world!"}
只支持一个应用程序的操作系统谈不上是一个合格的操作系统。上面的小实验都是执行一个程序，体现不出操作系统的强大能力。如果把两个执行程序放在一起执行会怎样呢？操作系统会如何管理这两个程序的执行呢？下面我们尝试把上述两个执行程序timing-helloworld和getchar-helloworld放在一起执行：
\begin{lstlisting}[language={bash}]
$ timing-helloworld & getchar-helloworld
[1] 24690
hello world!
hhello world!
uman
human, hello world!
hello world!
...
\end{lstlisting}

上述第一行命令的功能是让timing-helloworld和getchar-helloworld这两个程序都执行。所谓都执行，就是让操作系统来管理这两个程序的执行过程。让输入第一行命令并敲回车键后，shell程序请操作系统帮忙来执行这两个程序。操作系统收到请求后，先创建timing-helloworld程序的执行环境，再创建getchar-helloworld程序的执行环境，然后就开始执行这两个程序了。注意，每次操作系统只能调度一个程序占用CPU执行。在上面小节的分析中，已经描述过了两个程序的单独执行过程，但这里是两个程序一起执行。

当读者进行这个实验时，会碰到类似上面的输出现象，一开始两个程序都会由于等定时器或等用户输入而睡眠，所以操作系统会把两个程序设置为睡眠状态。一秒后，定时器会发出信号，操作系统收到定时器信号后，会唤醒timing-helloworld程序，让它继续显示字符串。在某一时刻，读者开始敲键盘，输入“human”。当输入第一个"h"时，getchar-helloworld程序被操作系统唤醒，并开始接收读者输入的字符。比较奇怪的是第四行的显示“hhello world!”和第五行的“uman”，这其实说明了在getchar-helloworld程序接受用户输入的时候，timing-helloworld程序的下一个一秒到期了，操作系统暂停了getchar-helloworld，切换到timing-helloworld程序继续执行，于是就出现了第四、五行的奇怪输出了。这里总算比较直观地能看到操作系统对多个执行程序执行过程的管理和调度了。

通过上面的五个实验，你会发现程序代码很简单，但默默付出的操作系统做了好多的幕后工作，但这些工作对于执行程序的用户而言都是很难直接看不到的，用户看到的是应用程序shell完成了用户的请求，而幕后英雄--操作系统只是默默的完成应用程序的各种请求。智人时代的操作系统的特点是麻烦自己，方便用户。把自己搞得特别复杂，像Linux kernel这样大家能看到源码的操作系统，其当前最新的4.17版本已经有２千万行代码了。即使是应用程序显示字符串这样一个简单过程，在Linux中执行了的代码行数也都过万行。但这不会影响我们了解其基本原理。

\subsubsection{哺乳动物时代下"hello　world!"}

虽然通过上面的五个小实验，我们对操作系统的功能有了一定的初步了解。但由于面向实际应用需求的Linux太复杂，使得初学者很难在有限的时间内深入到其内部，分析和理解其实现。
能否把操作系统的各种先进复杂的功能先丢到一遍，看看一个操作系统要在计算机上显示字符串，到底需要做哪些基本的事情呢？
回到三叶虫和恐龙时代，可以让我们看到操作系统最开始的原始面目，但当时的操作系统和硬件都太初级，无法体现我们现在操作系统的基本功能。而哺乳动物时代的操作系统代表UNIX是一个跨时代的操作系统，目前Linux、Windows的不少核心设计思想与UNIX有着千丝万缕的联系。

\begin{note} 
UNIX，是一个用C语言编写的多用户、多任务操作系统，支持多种处理器架构，最早由Ken Thompson、Dennis Ritchie和Douglas McIlroy于1969年在AT\&T的贝尔实验室开发，运行在PDP-7/11计算机上运。1975年UNIX　version 6(简称UNIX-v6)发表，它已经几乎具备了现代（单机）操作系统的所有概念：进程、进程间通信、多用户、虚拟内存、系统的内核模式和用户模式、文件系统、中断管理、I/O设备管理、系统接口调用、用户访问界面（shell）。
\end{note} 
	
如果设计了一个简单的操作系统，但运行在复杂的计算机系统上，比如现在基于x86的计算机上，依然会带来设计实现上的复杂性，这主要是由于硬件的复杂性造成的。有读者问，能否在不了解计算机硬件的情况下学习操作系统？我们觉得这是不太可行的。因为操作系统需要管理各种计算机中的硬件，包括CPU、内存、外设等，还需要通过对硬件的操作来管理应用程序。不涉及硬件操作的操作系统就是一个“空中花园”，看着很漂亮，但没有底子，摸不着，跑不起来。

即使是早期的UNIX-v6操作系统，也有一万行左右的代码，且基于古老的PDP计算机，使得分析,理解和运行UNIX-v6比较困难。其实我们可以对操作系统和计算机硬件系统进行深度简化，只保留阐述了基本原理级的操作系统代码，并建立一个的简化计算机系统(当然是用软件来模拟实现)，就可以让初学者比较容易理解且实践操作系统了。


我们构造了一个出入哺乳动物时代的简单操作系统mammal-os和与之配套的简单计算机mammal-computer，这个计算机只提供了基本的硬件能力，从而能够支持mammal-os的安全、多任务切换、中断处理、系统调用等类似Linux和Windows操作系统功的基本功能。

\paragraph{运行mammal-os}
我们可以先试试如何运行mammal-os。 首先，我们在智人时代的Linux操作系统环境把哺乳动物时代的mammal-os实验环境(参见\ref{setup})建立好。并在Linux环境中，执行特定编译命令，把mammal-os.c转换成执行程序mammal-os，并在mammal-computer模拟环境中执行生成的mammal-os操作系统和它管理下的两个应用程序：
\begin{lstlisting}[language={bash}]
$ make run
gcc -O3 -m32 -o ../tools/compiler ../tools/compiler.c -lm
gcc -O3 -m32 -o ../tools/mammal-computer \
../tools/mammal-computer.c -lm
../tools/compiler -o mammal-os mammal-os.c
../tools/mammal-computer mammal-os
hello hello ...  X  world! world! ... X  hello ...
\end{lstlisting}

在mammal-computer模拟器下，加载并执行mammal-os操作系统，可顺利地看到每次时钟定时中断产生时(可以通过'X'显示观察到)，两个执行程序交替执行，各自显示字符串“ hello “或" world! ”。这体现了mammal-os对外设的直接控制，对外设中断的响应，对应用程序提供的系统服务，对应用程序的管理与执行切换等操作系统的基本功能。接下来，需要分析一下mammal-computer到底是如何设计来支持mammal-os的，而mammal-os又是如何完成操作系统的基本功能的。

\begin{note} 
	这里没有用ucore-os的原因是，ucore-os是处于哺乳动物年代过渡时期的操作系统，完成一个显示字符串也许要一百行左右的代码，用在这里讲解还是复杂了一些。后续的讲解中，我们将基于ucore-os和risc-v操作系统来进行分析。
\end{note} 

\paragraph{mammal-computer}
现在我们需要设计一个计算机系统mammal-computer。mammal-computer是一个简化的计算机系统，包含一个简化的32-bit RISC　CPU--mammal-cpu，一块ram内存，时钟/屏幕两种外设。别担心，目前这个只支持mammal-os的CPU很简单。下面就其主要部分做简要介绍。

\subparagraph{寄存器}

\begin{itemize}
	\item reg\_a, reg\_b, reg\_c : 三个32-bit通用寄存器
	\item reg\_sp 为栈指针寄存器，按64-bit(8字节)对齐
	\item reg\_pc 为32-bit程序计数器（指向下一条指令），按32-bit(4字节)对齐
	\item reg\_flags - 内部状态寄存器(包括当前的运行模式,是否中断使能等)，可通过特定指令访问相关bit(如下所示)	
	\begin{itemize}
      \item  user bit: user bit为１:CPU处于用户态；为0:CPU处于内核态
      \item  iena bit: iena bit为１:使能中断;为0:屏蔽中断		
	\end{itemize}
\end{itemize}

mammal-cpu具有用户态(user mode)和内核态(kernel mode)两种特权级的运行模式。内核态的特权级别最高，用户态的特权级别最低。其中内核态运行模式是预留给操作系统使用的，可确保操作系统不受任何的限制地自由访问任何有效内存地址，执行特权（系统）指令，能直接访问外设，使用内核栈。而用户态运行模式是预留给给普通的应用程序使用的，运行于用户态的代码执行会被CPU安全保护机制监管，使用用户栈，不能执行特权（系统）指令，不能直接访问外设和其他一些特权指令。

\subparagraph{指令集}
mammal-cpu一条指令大小为32bit。整体来看,指令分为如下几类:
\begin{itemize}
\item 运算指令：如ADD, SUB等
\item 跳转指令：如JMP, JSR，LEV等
\item 访存（Load/Store）指令：如LL, LBL, LCL等
\item 系统命令:如HALT, RTI, IDLE，SSP, USP,IVEC, PDIR，目的是为了操作系统设计
\end{itemize}

mammal-os中涉及的mammal-cpu汇编代码都有比较详细的注释。如果读者还需进一步了解mammal-cpu的指令，可参见\ref{setupmammalcomp}中对指令集的描述。

\subparagraph{内存}
mammal-computer中包括一块连续的物理内存（RAM），与mammal-cpu直接相连，缺省内存大小为128MB，其物理内存地址是连续的，从0--128MB。

\subparagraph{外设}\label{mammaldevice}

mammal-computer只包含最基本的外设：timer(时钟)、 screen(屏幕)，支持中断响应和相关的IO操作。关于向屏幕输出字符的操作如下所示：
\begin{enumerate}
\item len --> reg\_a ：把输出的字符个数len给寄存器reg\_a，目前限制个数为1
\item chr --> reg\_b   ：把字符内容chr给寄存器reg\_b
\item BOUT指令：如果在内核态，输出一个字符chr；如果在用户态，产生异常
\end{enumerate}

关于时钟的操作，即设置时钟的超时(imeout)过程，如下所示：
\begin{enumerate}
\item val --> reg\_a  ：把timerout值给寄存器reg\_a
\item TIME指令 ：如果在内核态，设置时钟的timeout阈值为寄存器reg\_a的值；如果在用户态，产生异常
\item 当时钟增加了timeout值后会产生时钟中断
\end{enumerate}

\subparagraph{中断/系统调用}

时钟中断的建立过程如下所示：
\begin{enumerate}
\item 通过'TIME'指令设置时钟的time out阈值（>0）
\item 通过'IVEC'指令设置到中断处理例程的起始(入口)地址
\item 通过‘STI’指令使能中断
\end{enumerate}

这样就建立好了时钟中断。当时钟的tick增量超过了timeout阈值后，时钟外设就产生中断，使得mammal-cpu先把当前被打断执行的reg\_pc寄存器内容保存到内核栈中，然后再把中断号也保存到内核栈中，最后跳转到中断处理例程的起始地址处执行。注意，如果在用户态的应用程序执行TRAP指令，相当于通过软件产生了一个有特定编号的中断，mammal-cpu也要进行相应的压栈和跳转操作，这样中断和系统调用就可以统一处理了。

有了上面的介绍，大家就能进行理解和尝试下面将介绍的mammal-os操作系统了。

\paragraph{mammal-os}
mammal-os是一个假想的哺乳动物时代的极简OS，存在于操作系统的哺乳动物时代，运行在mammal-computer计算机系统上。基于mammal-computer计算机系统，mammal-os实现类似显示"hello　world!"、进行程序执行切换、响应中断、执行系统调用等的基本功能就会简单很多。虽然简单，其设计实现的基本思路与Linux、Windows这样的通用操作系统在本质上并无二致。接下来，我们来看看mammal-os这个操作系统在mammal-computer计算机系统上是如何完成这些工作的。mammal-os的大致执行过程包括：
\subparagraph{系统初始化}	

\begin{enumerate}
  \item 初始化时钟外设	
  \item 设置中断处理入口地址并使能中断
  \item 初始化应用程序执行环境
\end{enumerate}

\subparagraph{启动应用程序并响应中断和系统调用}

\begin{enumerate}
  \item 跳转到应用程序入口执行
  \item 当收到显示字符的系统调用请求后，访问显示外设，返回应用程序
  \item 当收到定时中断后，切换应用程序执行，返回新换入的应用程序
\end{enumerate}

接下来，将对访问外设、初始化应用程序执行环境、切换程序的执行上下文、应用程序执行系统调用、响应中断和系统调用等进行更细致的分析。

\subparagraph{访问外设}
mammal-computer计算机系统提供了两种基本的外设：显示输出和时钟。虽然不像基于x86的计算机那样强大，但足以支持mammal-os展现出主要的操作系统功能了。基于合理的“极简”原则，通过几条机器指令就可以完成对外设的管理和控制了(参见\ref*{mammaldevice})。通过下面的内核函数实现，可以很方便地控制显示输出和时钟定时设置。

\begin{lstlisting}[language={C}]
//通过向端口port写值val来控制外设
out(uint port, int val) {asm(LL,8);asm(LBL,16);asm(BOUT);}
//置时钟经过val个tick后将产生时钟中断
stmr(int val)   {asm(LL,8);asm(TIME);}
//执行HALT指令，停止计算机系统
halt(value)     {asm(HALT);}
\end{lstlisting}

这里需要注意的是，直接控制硬件会对计算机系统的整体安全性带了潜在的风险，基于操作系统对应用程序的不信任原则，需要在CPU硬件层面限制应用程序直接访问外设。所以，在mammal-computer的硬件设计上，把\textit{BOUT}指令和\textit{TIME}指令设置为特权指令，即只允许在CPU处于内核态时才能执行上述指令，如果在CPU处于用户态时执行这两条特权指令，会产生"越权"错误异常(privilege fault exception)。所以，如果应用程序想要访问外设，就需要通过其他方法(比如下面会介绍的系统调用)了。

\subparagraph{应用程序执行系统调用}
首先是应用程序，这里列出了两个程序progA和progB，分别显示" hello "字符串和" world! "字符串：
\begin{lstlisting}[language={C}]
//应用程序发出write系统调用（系统调用号是S_write），
//请求操作系统完成write系统服务
write() { 
　　//把外设端口port, char类型的字节序列和序列长度给寄存器; 
　　//执行write系统调用; 
　　asm(LL,8);asm(LBL,16);asm(LCL,24);asm(TRAP,S_write);}
//程序Ａ重复显示" hello "字符串
progA()　{while(current < 10) write(1," hello ",8);}
//程序B重复显示" world! "字符串
progB()　{while(current < 10) write(1," world! ",9);}
\end{lstlisting}

虽然都是显示字符串，但这里的程序相对与在Linux操作系统下的应用程序有一些不同。其中的显示字符串的write函数是用汇编语言实现的，前三句汇编代码大致内容是从栈中读出外设端口port, char类型的字节序列和序列长度，并分别赋值给寄存器reg\_a、reg\_b、reg\_c。最后一句汇编代码\textit{asm(TRAP,S\_write)}就是mammal-os实现的显示字符串内核函数的系统调用接口(system call　interface)，即把系统调用号S\_write压栈并执行\textit{TRAP}指令，这样应用程序就可以向操作系统发出显示字符串的系统调用请求了。

为什么应用程序不直接通过函数调用来访问操作系统提供的函数呢？因为操作系统内核不信任应用程序，它的一个前提假设是“应用程序不可信，bug太多，有安全隐患”。所以虽然操作系统内核提供了一系列特定功能的内核函数，但应用程序不能直接通过函数调用来访问，而不得不用开销大一些，但有比较全面的安全措施的系统调用接口来访问。

\begin{note} 
操作系统内核通过一组特殊的函数访问接口把内核函数呈现给应用程序，使得能够让应用程序基于此接口发出的请求并让操作系统执行，执行完毕后可返回应用程序，这个接口就是\emph{系统调用(system call)}接口\label{syscall}，这可以看成是一种涉及特权级改变的特殊的函数调用接口。系统调用指令本身的性能开销就比一般的函数调用要多一些，是因为在硬件和软件层面要进行一些额外执行上下文的保存、安全检查等。
\end{note} 

为什么Linux操作系统下的helloworld应用程序没有这些让人看不懂的汇编代码？那是因为helloworld应用程序调用的puts函数是被glibc封装了起来，在在glibc库中的puts函数实现比较复杂，而puts函数最终也要执行Linux的write系统调用(C代码+汇编代码)来请操作系统完成显示字符的操作。

mammal-os的系统调用接口提供了哪些安全措施呢？这其实要看mammal-cpu对\textit{TRAP}指令的硬件实现了。比如，当应用程序progA执行\textit{TRAP}指令时，mammal-cpu会从用户特权态切换到内核特权态(在mammal-cpu中reg\_flag的user bit确定为0)，且把reg\_sp指向内核栈progA\_kstack了，接下来在操作系统中执行函数调用就会用内核栈了，避免了使用用户栈可能导致的内核信息泄露等安全隐患。其实，mammal-cpu的安全检查还不够，在实际计算机系统中，比如基于x86,risc-v的Linux等，CPU和操作系统还会有更加严格的安全检查和安全设置。


\subparagraph{初始化应用程序执行环境}
为了支持应用程序正常执行，mammal-os操作系统需要建立应用程序的运行环境。一个应用程序的基本运行环境包括：用户栈空间；用户态的开始执行位置；内核栈空间；内核态的开始执行位置。为了让应用程序progA和progB能定时切换执行，而且还需要初始化时钟的timeout值，使得可以过一段时间就换一个程序执行；通过建立响应中断和系统调用的入口位置，使得mammal-cpu知道当产生时钟中断或系统调用后，需要跳转到哪里进行处理，并可进一步进行程序执行切换和显示字符串等工作。

\begin{lstlisting}[language={C}]
//程序progA和progB的用户栈，内核栈和栈当前位置指针
#define N 1000
char pa_stack[N], pa_kstack[N],pb_stack[N], pb_kstack[N];
int *pa_sp,*pb_sp;
//mammal-os的入口函数
main() {
  int *kstack;　 //用于存储当前内核堆栈的位置　　　　　　　　　　　　　　 
  stmr(5000);　　//设置时钟到期的值为5000tick
  ivec(alltraps);//设置中断/系统调用的入口函数为alltraps
  //初始化progA执行程序的内核栈和用户态下执行环境
  pa_sp = &pa_kstack[N];  //设置progA的内核栈栈底
  pa_sp -= 2; *pa_sp = &progA;//设置progA在用户态执行的起始地址
  pa_sp -= 2; *pa_sp = USER;  //设置progA的CPU用户态标志
  pa_sp -= 2; *pa_sp = 0; 　　//设置progA用户态下的reg_a的值为0
  pa_sp -= 2; *pa_sp = 0; 　　//设置progA用户态下的reg_b的值为0
  pa_sp -= 2; *pa_sp = 0; 　　//设置progA用户态下的reg_c的值为0
  pa_sp -= 2; *pa_sp = &pa_stack[N];//设置progA用户态下的用户栈
  pa_sp -= 2; *pa_sp = &trapret;//设置progA下一执行地址为trapret  
  //初始化progB执行程序的内核栈，使得progB能在用户态执行  
  pb_sp = &pb_kstack[N];   //设置progB的内核栈栈底
  pb_sp -= 2; *pb_sp = &progB;//设置progB在用户态执行的起始地址
  pb_sp -= 2; *pb_sp = USER;  //设置progB的CPU用户态标志
  pb_sp -= 2; *pb_sp = 0;     //设置progB用户态下的reg_a的值为0
  pb_sp -= 2; *pb_sp = 0;     //设置progB用户态下的reg_b的值为0
  pb_sp -= 2; *pb_sp = 0;     //设置progB用户态下的reg_c的值为0
  pb_sp -= 2; *pb_sp = &pb_stack[N];//设置progB用户态下的用户栈
  pb_sp -= 2; *pb_sp = &trapret;//设置progB下一执行地址为trapret
  //建立让progA执行的内核执行环境
  kstack = pa_sp;
  //reg_a=kstack;reg_sp=reg_a，此时栈指针sp指向progA的内核栈
  asm(LL,4); asm(SSP);
  asm(LEV);}//根据sp保存的内容，main函数返回到trapret继续执行 
//构造回到用户态的执行环境
trapret(){
　　asm(POPA);asm(SUSP);　　　　　//从内核栈恢复用户态栈位置
　　asm(POPC);asm(POPB);asm(POPA);//从内核栈恢复寄存器内容
　　asm(RTI);}　　　　　　　　　　//中断/系统调用返回
\end{lstlisting}

\begin{note} 
栈（stack）— 栈是一块内存空间，是实现函数调用的重要机制。由编译器自动分配释放栈空间，在栈空间中存放函数的参数值，局部变量的值等。其操作方式类似于数据结构中的栈。用户栈和内核栈分别是应用程序在执行过程中在用户态和内核态时用的栈(靠堆栈指针寄存器切换)。
\end{note} 

首先（Line3-4），我们明确定义了程序progA和progB用到的栈空间。这里还分别定义了用户栈progA\_stack、progB\_stack，以及内核栈progA\_kstack、progB\_kstack。为什么执行一个应用程序需要两个栈？简单的回答是为了安全。其主要原因是应用程序被操作系统认为是不可靠的，通过把应用程序限制在用户态(在mammal-cpu中reg\_flag的user bit确定为1)下运行，使得应用程序不能直接调用操作系统实现的函数，不能直接访问硬件和执行特权指令，从而不能破坏操作系统。为什么Linux操作系统下的应用程序看不到关于这两个栈的定义？简单的回答是因为你没看Linux kernel的源代码，在Linux kernel中有关创建程序执行环境的源代码中，建立了用户栈和内核栈。

接着(Line10)，初始化时钟外设的timeout值为5000　　ticks；并设置中断/系统调用的入口函数为alltraps，这样产生中断或系统调用后，mammal-cpu知道需要跳转到alltraps函数完成相应的处理。

何时需要使用内核栈呢？当应用程序执行系统调用请求或产生中断的时候需要用到内核栈。在mammal-cpu的硬件设计中，当应用程序执行行\textit{TRAP}指令后，mammal-cpu会从用户特权态切换到内核特权态，且返回地址，中断号压入内核栈保存，把reg\_pc寄存器指向中断入口地址，即alltraps函数地址。

建立应用程序的基本运行环境需要一些特殊的处理。首先是设置内核栈空间，并在内核栈空间中压入一系列的值，包括:
\begin{enumerate}
	\item USER：执行RTI指令时，会弹栈把USER表示的整数赋值给reg\_flag寄存器，使得mammal-cpu处于用户态；
	\item 三个0：执行trapret函数时，弹栈给寄存器reg\_a、reg\_b、reg\_c；
	\item 用户栈地址：执行trapret函数时，弹出并赋值给reg\_usp用户栈寄存器；
	\item trapret函数地址：应用程序在内核态的执行地址入口
\end{enumerate}

main()函数的最后两行代码也是容易让人迷惑的汇编代码，这里读者只需理解其大致含义即可，即调整栈指针指向刚才设置好的progA的内核栈(注意，此时栈顶指针指向的内存单元的内容是trapret函数地址)，这样当执行\textit{LEV}指令完成函数返回操作时，mammal-cpu弹栈，把reg\_pc寄存器赋值为栈顶指针指向的trapret函数地址，开始进行progA为了能在用户态执行所做的内核特殊准备。

\begin{note} 
	\textit{RTI}指令的实现步骤:从当前内核栈中弹出中断码、reg\_pc寄存器；如果中断码的USR位(第16位)为1，表示当前是从内核特权态转到用户特权态，则设置reg\_flag寄存器的user位为１(表示CPU处于户特权态)，把reg\_sp寄存器指向用户栈，并跳转到reg\_pc寄存器指向的地址继续执行。
\end{note} 

为了能在用户态执行progA应用程序的用户函数progA()，mammal-os还需帮助progA应用程序完成最后一个准备步骤，即从内核态转换用户态并跳转执行用户函数progA()，这具体实现在trapret()函数中。读者也许会问，为何不直接函数调用progA()?这当然是可以做到的，但这样进行函数调用后，progA()是运行在内核态的，而我们假定为了安全，应用程序是必须允许在用户态。

对于完全用汇编语言编写的trapret()函数，它到底做了啥？这里简单解释一下，在main函数中，reg\_sp栈寄存器指向了progA的内核栈，且从栈顶向下，依次压入了trapret函数的地址、progA的用户栈地址、三个0、表示用户态的USR整数值、用户函数progA()的地址。当main函数返回时，从当前栈顶弹出了trapret函数的地址给reg\_pc寄存器，使当前cpu跳转到trapret函数开始执行。trapret函数首先弹出了用户栈地址并执行\textit{SUSP}特权指令设置内部的用户态栈指针(应用程序不可见)；然后弹出三个0，分别赋值给reg\_c、reg\_b、reg\_a，这表示progA应用程序开始执行时，所有的通用寄存器都为0；在接着执行\textit{RTI}指令，根据内核栈中的USR整数值和用户函数progA()的地址，转换CPU用户态，设置reg\_sp为内部的用户态栈指针，设置reg\_pc寄存器为用户函数progA()的地址。这样mammal-cpu在接下来的执行中，就是在执行用户态的progA程序了。

\subparagraph{响应中断和系统调用}\label{dointr}

如果没有中断机制，当外设产生了一个事件(比如时钟的定时器到期了)后，如果操作系统不去访问外设，操作系统是无法知道这个外设事件是否发生。所以操作系统不得不以轮询的方式(一般是在一个while循环中查看外设事件是否发生)来查询外设事件是否发生。很明显，如果操作系统一边查询外设事件，同时还要做其他事情，那效率是比较低的。而且通过中断机制，操作系统还可以强制打断应用程序的执行(即使应用程序不想交出CPU使用权)，把CPU控制权交给操作系统设计实现的中断处理例程来进行各种操作，比如切换多个应用程序的执行等，这是操作系统能有效管理应用程序的一种直接体现。

\begin{note} 
执行上下文(execution-context)是CPU正在执行时的硬件环境，包括各种寄存器信息和CPU状态(比如当前的CPU特权态)。
\end{note} 

中断是外设通知操作系统的一种事件机制，当然这需要CPU的硬件支持。站在mammal-computer计算机系统的角度看，假设一个应用程序正在用户态执行，这时时钟外设的定时到期(定时值通过\textit{TIME}指令设置)，会产生一个中断信号，通知mammal-cpu有中断事件了。mammal-cpu收到此中断信号后，会打断当前执行，保存被打断应用程序的执行上下文(包括中断号|特权态的整数值、被打断时的reg\_pc寄存器值)到内核栈中，跳到响应中断和系统调用的函数入口(中断处理入口地址通过\textit{IVEC}指令设置)去执行中断服务例程。mammal-os执行中断服务例程后，还需恢复被打断的执行上下文(包括当时的特权态和reg\_pc寄存器值)。最后被打断的应用程序会继续执行，就像啥都没发生一样。上一小节介绍了对时钟定时的设置和中断处理入口地址的设置，这里我们来看看mammal-os是如何响应中断和系统调用的。

\begin{lstlisting}[language={C}]
int current;//全局变量
//中断/系统调用的入口函数，此时内核栈中已经保存了被打断的
//pc寄存器值和中断/系统调用号
void alltraps(void){
　asm(PSHA);asm(PSHB);asm(PSHC);//通用寄存器值压到内核栈
　asm(LUSP);asm(PSHA);          //用户栈地址压到内核栈
　trap();                       //调用中断处理主体函数
　asm(POPA);asm(SUSP); 　　　　 //从内核栈弹出恢复用户栈
　asm(POPC);asm(POPB);asm(POPA);//从内核栈弹出恢复寄存器
　asm(RTI);}                    //从中断处理返回
//sys_write系统服务实现，把字节序列写到端口port处
sys_write(uint port, char *p, n) 
{int i;for (i=0;i<n;i++) out(port,p[i]);return i;}
//中断/系统调用主体处理函数
trap(int *sp,int c,int b,int a,int intrnum,unsigned *pc){
  switch (intrnum) {
  case FSYS + USER: //来自应用程序的系统调用
    switch (pc[-1] >> 8) {//取出系统调用号
    //如果系统调用号是S_write，则执行系统服务sys_write
    case S_write: a = sys_write(a, b, c); break;
    default:  asm(HALT);} //其他情况下，停止计算机系统
    break;   
  case FTIMER:  
  case FTIMER + USER://来自时钟中断
　　　out(1,' ');out(1,'X');out(1,' ');//显示" X "
　　　//如果current是奇数，则换出progA，换入progB
　　　if (++current & 1)　task_switch(&pa_sp, pb_sp);
   　//如果current是偶数，则换出progB，换入progA
   　　else　 　　　　　　task_switch(&pb_sp, pa_sp);
  　　break;
  default: asm(HALT);}} //其他情况下，停止计算机系统
\end{lstlisting}

我们又看到了一个几乎都是汇编代码构成的重要函数alltraps()，因为这个函数是响应中断和系统调用的入口(在mammal-os的main()函数中，通过\textit{IVEC}指令设置中断处理入口为alltraps的地址)。mammal-cpu在收到外设中断信号或执行\textit{TRAP}指令时，就会转换为内核态，采用内核栈，并跳转到alltraps()函数。alltraps()函数的汇编代码主要做的事情就是把被打断的执行上下文保存到内核栈中，并调用trap()函数完成对中断和系统调用的处理后，从内核栈中恢复被打断的执行上下文，；最后通过\textit{RTI}指令回到以前被打断的执行上下文处，能够继续执行。

而第二个函数trap()是中断/系统调用主体处理函数，其执行过程主要分为两部分，第一部分是处理来自应用程序的write系统调用，如果是write系统调用，则调用sys\_write()内核函数，并最终会执行\textit{BOUT}指令，通过给端口写字符来显示字符；如是其他未知系统调用，则停止计算机系统。第二部分是处理来自外设的中断信号，如果发现打断内核态或用户态执行过程的中断是时钟中断(表示时钟定时器到期)，则根据current的奇偶性，进行progA应用程序与progB应用程序的切换(具体切换过程会在下面分析)；如果是其他未知中断，则停止计算机系统。

\subparagraph{切换程序的执行上下文}
在计算机系统中只有一个应用程序执行的情况只存在于远古的三叶虫时代和恐龙时代。在mammal-computer的支持下，mammal-os能够支持多个应用程序分时使用CPU，定时切换执行，这样能更加充分的使用mammal-computer计算机资源。这里的一个关键是要能够切换不同程序的执行上下文(execution-context)。下面来看看mammal-os是如何实现这个切换功能的。

\begin{lstlisting}[language={C}]
//切换程序的执行上下文
//1.保存被换出的程序的栈指针到此程序的栈当前位置的指针(这里是old)
//2.把被换入的程序的栈指针恢复为上次保存的栈当前位置指针内容
//3.函数返回时，根据栈指针保留的返回地址，将执行被换入的程序
prog_switch(int *old, int new) 
{
  asm(LEA, 0); //step1: reg_a = reg_sp　　
  asm(LBL, 8); //step1: reg_b = old
  asm(SX, 0);  //step1: *reg_b = reg_a
  asm(LL, 16); //step2: reg_a = new
  asm(SSP);}   //step2: reg_sp = reg_a
\end{lstlisting}
 
prog\_switch又是一个全用汇编编写的重要内核函数，主要功能是进行换出程序的内核栈顶地址的保存，以及恢复换入程序的内核栈顶地址，恢复完后，执行函数返回指令，就会从新的栈顶中取出返回地址，开始进行换入程序在内核态中的执行。

这个函数被上面分析过的trap()内核函数调用，如调用task\_switch(\&pa\_sp, pb\_sp)，调用参数是progA应用程序的内核栈地址pa\_sp的地址和progB的内核栈地址pb\_sp。这样，prog\_switch第一个形参是要被换出的应用程序内核栈地址的指针，第二个形参是要换入的应用程序的内核栈地址。具体的执行分析如下所示：
\begin{lstlisting}[language={C}]
1. reg_a = reg_sp　当前执行程序的当前内核栈顶地址
2. reg_b = old 用于保存当前执行程序的执行上下文内存地址
3. *reg_b = reg_a　在执行上下文内存地址中保存当前内核栈顶地址
4. reg_a = new　从当前内核栈中取出要换入程序的执行上下文内存地址
5. reg_sp = reg_a　　从换入程序的执行上下文内存地址读出换入程序的内核栈顶并设置为reg\_sp
\end{lstlisting}

完成上述准备后，最后是执行函数返回指令，即从换入程序的内核栈顶(即reg\_sp寄存器指向的地址)中读出换入程序的返回地址并赋值给reg\_pc寄存器，从而开始了新换入程序的执行。


